# latexTable.R
# created 2012 July 21


latexTable <- function(
  mat, 
  SETable             = TRUE,
  starredFloat        = FALSE,
  rowNames            = rownames(mat), 
  colNames            = colnames(mat)[seq(1, ncol(mat), by = 2)],  # odd colnames(mat)
  colNameExpand       = FALSE,
  formatNumbers       = TRUE,  
  decimalPlaces       = 2,  
  NAtext              = '',
  columnTierSeparator = '  ',
  extraRowHeight      = if (SETable) { '2pt' } else { '4pt' },
  tabcolsep           = '2.75pt',
  hspace              = '-0in',
  headerFooter        = TRUE,
  spaceBetweenColnameRows = TRUE, 
  landscape           = if (SETable) { ncol(mat) / 2 >= 6 } else { ncol(mat) >= 6 },
  SE_fontSizeString   = '\\fontsize{10.3bp}{10.3bp}\\selectfont',
  spacerColumns       = NULL,
  spacerColumnsWidth  = '.5em',
  spacerRows          = NULL,
  spacerRowsHeight    = '.15in',
  footerRows          = if (is.null(rowNames)) { NULL } else { c('Number of observations', rep('000', ncol(mat)/2)) },
  printCaption        = TRUE,
  caption             = paste0('\\', label, 'Caption'),
  captionMargins      = NULL, 
  commandName         = 'myTable',
  label               = commandName,
  callCommand         = TRUE,
  writeToClipboard    = Sys.info()['sysname'] == 'Windows') {
  
  # This function pretty-prints a ready-for-LaTeX table in which the cells are 
  # decimal-aligned, the entries are rounded to the specified decimal place, 
  # and the standard errors appear in smaller print than their estimates.  
  #   If headerFooter == TRUE, it returns the same table wrapped in a 
  # LaTeX command.  [2012 07 22]
  #   If SETable == TRUE (the default), even-numbered columns are assumed to  
  # be standard errors.  They are printed in smaller type.  [2012 09 07]
  #   The LaTeX code printed by this function assumes that the array, 
  # numprint, and booktabs packages have been loaded in LaTeX.  [2012 07 23]
  #   Some tweaking of the output by hand may still be necessary to get the 
  # desired appearance.  In particular, tweaking of the numprint column 
  # specifications may be necessary.  For example, if a column pair has a wide 
  # colName label, you may need to change N{2}{2} in the estimate-column  
  # specification to N{3}{2} or N{4}{2} to get the column pair centered   
  # beneath its colName.  [2012 07 24]
  
  # mat: matrix to print
  # SETable: indicates whether the table contains estimate-SE column "tiers."
  #   If FALSE, the function assumes that each column stands on its own, 
  #   bearing no special relation to any other column.  [2012 09 07]
  # starredFloat: use table* instead of table, to ensure that table crosses
  #   columns.  [2015 12 28]
  # rowNames: character vector that provides row labels for the output table.  
  # colNames: list, or object that can be coerced to a list, of column labels
  #   for the output table.  If the list has multiple entries, the elements in
  #   the first entry will appear above the elements for the second entry, 
  #   which will appear above the elements for the third entry, and so on.
  #     The default for colNames is to take the odd-numbered values of 
  #   colnames(mat).  
  #     colNames has no effect unless headerFooter == TRUE.
  # colNameExpand: I often want a single column name to bridge multiple 
  #   columns.  If colNameExpand == TRUE and a column name in the colNames 
  #   vector is followed by one or more empty entries (''), the column name  
  #   will bridge the corresponding columns.  
  # formatNumbers: pretty-print the entries in mat, e.g., by adjusting the  
  #   number of digits after the decimal place.  If FALSE, no adjustment will 
  #   be done.  
  # decimalPlaces: entries will be shown to this decimal place.  Note that 
  #   this affects integers: "3" becomes "3.00".  Only has effect if 
  #   formatNumbers == TRUE.
  # columnTierSeparator: in the LaTeX code, all columns are separated from 
  #   each other by " & ".  Column tiers (i.e., pairs of columns giving the  
  #   estimate and the SE of the estimate for a particular coefficient) are  
  #   also separated by columnTierSeparator.  This option affects only the 
  #   LaTeX code -- not the typeset (e.g., in PDF) version of the table.
  # tabcolsep: character vector indicating a length that LaTeX recognizes,
  #   e.g., ".25in".  The \tabcolsep value in LaTeX, which is half the 
  #   intercolumn space, will be set to this value if headerFooter == TRUE.   
  #   This will be the distance between estimate and SE columns in each
  #   column pair.  
  # headerFooter: determines whether to print extensive LaTeX code above and 
  #   below the table rows.  If headerFooter == TRUE and colNames == TRUE, the
  #   assumption is that mat contains pairs of columns.  The colNames will be
  #   centered over each pair of columns: colName[1] will be positioned over 
  #   mat[, 1:2], and so on.  
  # spaceBetweenColnameRows: when the column names in a table take up multiple
  #   rows, should vertical space between the rows be added?  For PNAS tables,
  #   I don't want any such space.  For my normal formatting, I do.  
  #   [2015 12 28]
  # landscape: determines whether table is printed as landscaped or portrait.
  #   Only affects the output if headerFooter == TRUE and callCommand == TRUE.
  # spacerColumns: a vector of columns in mat after which to add columns that
  #   do nothing but insert horizontal space into the typeset table.  Using 
  #   "spacer columns" is an inefficient way to put horizontal space between
  #   columns, but relative to the other methods, it allows finer control 
  #   over the appearance of the typeset table.  To add a spacerColumn between
  #   the rownames and the first data column, make 0 one of the values in 
  #   spacerColumns.
  #     Ordinary methods for inserting space between columns involve the 
  #   \tabcolsep and \extracolsep LaTeX lengths.  Unfortunately, \cmidrule and
  #   other booktabs rules don't recognize that these spaces are -between-
  #   columns.  As a results, rules (horizontal lines) drawn by booktabs 
  #   extend well into the intercolumn region if \tabcolsep and \extracolsep
  #   are used to provide intercolumn space.  A similar problem occurs if
  #   \hspace is used to provide intercolumn space.
  # spacerColumnsWidth: either a single string of a recognizable LaTeX length
  #   (e.g., '.5em') or a character vector indicating the width of each 
  #   spacer column.
  # spacerRows: integer-valued vector.  After each row in mat whose number is 
  #   in spacerRows, a vertical space of spacerRowsHeight will be printed.  
  #   For example, if spacerRows == c(2, 4), a vertical space will be added 
  #   after rows 2 and 4 of mat.  
  # commandName: name of the command for the LaTex table. 
  # label: label for the LaTeX figure.  Goes in the caption.
  # callCommand: determines whether the last line of output is a call to the 
  #  command that creates a table.  With default settings, this will be 
  #  "\myTable{p}}}".
  # writeToClipboard: copy entire output to the Windows clipboard.
  
  # Programming notes:
  # --str_pad() cannot handle NA values.  They will need to be converted to 
  #   something else (e.g., "NA") before being passed to str_pad().
  #   [2012 07 22]
  
  require(Bullock, lib.loc = c(.libPaths(), 'packageLibrary'))  # for unshift
  require(stringr)  # for str_pad, str_wrap
  
  # GET PRELIMINARY INFORMATION  
  # These seemingly redundant lines are important.  Without them, changes to 
  # rownames(mat) and colnames(mat) will change rowNames and colNames, 
  # respectively, provided that the user doesn't specify the rowNames or 
  # colNames arguments.  The principle seems to be "default arguments that are
  # functions of other arguments can be modified until they are first called."
  # In other words, there is some very lazy evaluation at work.  [2012 07 22]
  rowNames   <- rowNames
  colNames   <- colNames 
  if (! is.null(colNames)) {
    colNames <- if (is.list(colNames)) colNames else list(colNames)
  }
  footerRows <- footerRows
  if (! is.null(footerRows)) {
    footerRows <- if (is.list(footerRows)) footerRows else list(footerRows)
  }
  landscape  <- landscape
  nrow       <- nrow(mat)
  ncol       <- ncol(mat)
  if (formatNumbers) {
    mat <- round(mat, decimalPlaces)
  }
  
  # CHECK ARGUMENTS
  if (SETable && !is.null(colNames) && length(colNames[[1]]) != ncol/2) {
    stop("length of colNames[[1]], ", length(colNames), ", is not half of ncol(mat).")
  }
  if ( any(grepl('&', rowNames)) ) {
    if (! is.null(spacerColumns)) {
      stop("spacerColumns is non-NULL and there are ampersands (perhaps escaped ampersands) in your rowNames.  This is a recipe for havoc.")
    }
    else {
      warning("the ampersands in your rowNames could screw up the table, even if they are escaped.")
    }
  }  
  if (grepl('&', columnTierSeparator)) {
    warning(str_wrap("columnTierSeparator includes an ampersand.  This is likely to screw up the layout of your table.", 72, exdent = 2))
  }
  if(!is.null(spacerColumns) && max(spacerColumns) >= ncol) {
    stop("max(spacerColumns) must be less than ncol(mat).")
  }
  if (!is.null(spacerColumns) && (ncol %% 2 != 0) && headerFooter) {
    warning("spacerColumns is non-NULL, ncol(mat) is odd, and headerFooter == TRUE.  This combination of options is unlikely to produce a table that will work in LaTeX.")
  }
  if (!is.null(captionMargins) && length(captionMargins) != 2) {
    stop("length(captionMargins) must be NULL or equal 2.")
  }
  
  
  # ADJUST DIGIT SETTINGS  [2014 4 021]
  oldDigitsOption <- as.integer(options("digits"))
  options(digits = decimalPlaces + 1)
  on.exit(options(digits = oldDigitsOption))
  z <- paste(rep(0, decimalPlaces), collapse = '')  # zeroes to append
  
  
  # WORK ON THE MATRIX ROW BY ROW
  # First, convert each row to a string.  Then operate on the string.  
  # [2012 07 22]
  if (formatNumbers) {
    for (i in 1:nrow(mat)) {
  
      # PAD OUT ENTRIES WITH TRAILING ZEROES.  [2014 06 21]  
      matChar <- as.character(mat[i,])
      matChar <- gsub('^0$', paste0('0.', z), matChar)                  # take care of entries that are simply "0"
      matCharAfterDecimal <- gsub('-?\\d+\\.', '', matChar)             # get characters after the decimal place, e.g., "12"
      decimalPlacesToAdd <- decimalPlaces - nchar(matCharAfterDecimal)  # e.g., add two zeroes to one entry, zero zeroes to another
      for (ind in 1:length(matChar)) {
        if (is.na(matChar[ind])) { next }
        matChar[ind] <- str_pad(
          string = matChar[ind],
          width  = nchar(matChar[ind]) + decimalPlacesToAdd[ind],
          side   = 'right',
          pad    = '0')
      }
      matLine <- paste(matChar, collapse = ' & ')
      
      
      # ADJUST FORMATTING OF NUMBERS
      # This is a much older attempt to pad out entries with trailing zeroes and
      # to process the entries in other ways.  It replaces '0' with '0.00', '123' 
      # with '123.00', etc.  [2011 02 17]     
      if (SETable) {
        matLine <- sub('^(-?\\d+)\\s',    paste0('\\1.', z, ' '),  matLine)  # If the first entry in a row is X, this changes it to, e.g., X.00
        matLine <- gsub('\\s(-?\\d+)\\s', paste0(' \\1.', z, ' '), matLine)  # For subsequent entries, replace ' 1 ' with ' 1.00 ', ' -1 ' with ' -1.00 ', etc.
        matLine <- gsub('\\s(\\d+)$',     paste0(' \\1.', z),      matLine)
      }
      
      # Remove leading zeroes.  [2011 12 17]
      matLine <- gsub('^0\\.',   ' .', matLine)
      matLine <- gsub('\\s0\\.', ' .', matLine)
      matLine <- gsub('-0\\.',   '-.', matLine)
      
      # Replace, e.g., '.1' with '.10' [2011 02 17]
      matLine <- gsub('(\\.\\d)\\s', '\\10 ', matLine)
      matLine <- gsub('(\\.\\d)$',   '\\10', matLine)
      
      # Replace NA values [2012 07 22]
      if (! is.null(NAtext)) {
        matLine <- gsub(' NA', paste0(' ', NAtext), matLine)
        matLine <- gsub('NA',  paste0(' ', NAtext), matLine)
        matLine <- gsub(' & $', ' &  ', matLine)  
        # w/o this, strsplit will return vector of ncol - 1 (too short) 
      }
      
      # Replace the values in mat[i, ] with the new text-processed values from 
      # matLine.  [2012 07 22]
      mat[i, ] <- strsplit(matLine, ' & ', fixed = TRUE)[[1]]
    }
  }
  
  # PAD ENTRIES SO THAT ALL ENTRIES IN COLUMN HAVE EQUAL WIDTH  [2012 07 22]  
  # Column padding needs to be done after all of the text substitutions have 
  # been made -- taking out leading zeroes, etc.  This is padding in the 
  # columns that appear in the .tex file, not padding in the columns that 
  # will ultimately appear in the PDF file.  [2012 07 22]
  #
  # In the mapply() command, str_pad() works row-wise -- not column-wise -- on
  # the matrix that it receives.  But the column widths need to be applied 
  # column-wise.  This is why I transpose the matrix when passing it to 
  # mapply, then transpose it back again with the matrix() command.  
  # [2012 07 22]
  colWidths <- apply(mat, 2, function (x) max(nchar(x))) 
  mat <- mapply(str_pad, t(mat), width = colWidths, SIMPLIFY = TRUE)
  mat <- matrix(t(mat), nrow, ncol, byrow = TRUE)
  
  
  # CREATE OUTPUT VARIABLE
  # outputStrings is a vector of strings.  It holds the LaTeX code.
  # [2014 03 14]
  outputStrings <- NULL 
  
  
  # CREATE HEADER
  if (headerFooter) {
    
    # colDest is a vector describing the type of each column that comes after  
    # the column of rownames (if there is a column for the rownames).  It 
    # takes the values "est", "SE", or "spacer".  It's used for column 
    # specification and for\cmidrule commands.  [2012 07 24]
    if (SETable) {
      colDest <- rep(c("est", "SE"), ncol/2)  #
    } else {
      colDest <- rep(c("est"), ncol)  #
    }
    if (! is.null(spacerColumns)) {
      for (i in rev(sort(spacerColumns))) {
        colDest <- append(colDest, "spacer", after = i)  
      }
    }    
    
    # Remove everything from decimal point on.  Then replace all NA cells 
    # with ''.
    tmpRegex <- paste0('\\s*', NAtext, '\\s*')
    if (nrow(mat) == 1) {
      tmp <- gsub('\\.\\d*', '', mat)                  
      tmp <- gsub(tmpRegex,  '', tmp)
    }
    else {
      tmp <- apply(mat, 2, function (x) sub('\\.\\d*', '', x))
      tmp <- apply(tmp, 2, function (x) sub(tmpRegex, '', x))
    }
    
    # leadingDigits is used for the numprint column specifications.  [2012 07 24]
    leadingDigits <- apply(tmp, 2, function (x) max(nchar(x)))                # get number of digits before decimal point in each data column 
    leadingDigits <- new_stack(leadingDigits)                                 # make leadingDigits work with unshift()
    
    # Process spacerColumnsWidth.  If a different spacer column width was 
    # specified for each column, make spacerColumnsWidth into a stack from 
    # which I can unshift values.  [2012 08 05]
    spacerColumnsWidth <- rep(spacerColumnsWidth, length(spacerColumns))      # no effect if spacerColumnsWidth already has correct length.    
    spacerColumnsWidth <- new_stack(spacerColumnsWidth)
    
    # Start to write the header.
    outputStrings <- c(outputStrings, paste0('\\newcommand\\', commandName, '[1]{'))
    if (starredFloat) { 
      outputStrings <- c(outputStrings, '  \\begin{table*}[#1]')
    } else {
      outputStrings <- c(outputStrings, '  \\begin{table}[#1]')      
    }
    outputStrings <- c(outputStrings, paste0('    \\setlength{\\extrarowheight}{', extraRowHeight, '}'))
    outputStrings <- c(outputStrings, '    \\begin{center}')
    if (! is.null (hspace)) {
      outputStrings <- c(outputStrings, paste0('      \\hspace*{', hspace, '}'))
    }
    outputStrings <- c(outputStrings, paste0('      \\setlength{\\tabcolsep}{', tabcolsep, '}'))
    outputStrings <- c(outputStrings, '      \\begin{tabular}{%')
    if (! is.null(rowNames)) { 
      outputStrings <- c(outputStrings, '        r%')
    }
#    if (is.null(spacerColumns)) {
#      outputStrings <- c(outputStrings, paste0('        *{', ncol/2, '}{'))
#      outputStrings <- c(outputStrings, '          N{1}{2}%')
#      outputStrings <- c(outputStrings, '          >{{\\fontsize{10.3bp}{10.3bp}\\selectfont}}N{1}{2}%')
#      outputStrings <- c(outputStrings, '          @{\\hspace{1.5em}}')
#      outputStrings <- c(outputStrings, '        }')
#    }
    for (i in colDest) {
      if (i == 'est' && SETable) {
        outputStrings <- c(outputStrings, paste0('        >{{\\hspace*{0em}}}N{', unshift(leadingDigits), '}{', decimalPlaces, '}%'))        
        # outputStrings <- c(outputStrings, paste0('        >{{\\hspace*{0em}}}N{', unshift(leadingDigits), '}{2}%'))
      } 
      else if (i == 'est' && !SETable) {
        outputStrings <- c(outputStrings, paste0('        >{{\\hspace*{0em}}}N{', unshift(leadingDigits), '}{', decimalPlaces, '}%'))        
      }
      else if (i == 'SE') {
        outputStrings <- c(outputStrings, paste0('        >{{', SE_fontSizeString, '}}N{', unshift(leadingDigits), '}{', decimalPlaces, '}%'))
      }
      else if (i == 'spacer') {
        outputStrings <- c(outputStrings, paste0('        p{', unshift(spacerColumnsWidth), '}%'))
      }
    }
    outputStrings <- c(outputStrings, '      }')  # ends "\begin{tabular}{"    
    if (! is.null(colNames)) {      
      
      # Add \multicolumn commands for colNames.  [2012 07 23]
      #   Recall that colNames is a list with multiple entries, to allow for 
      # column headings that span multiple lines.  So mcRow is a "row" in the
      # column headings.  [2014 04 27]
      for (i in 1:length(colNames)) {
        mcRow <- colNames[[i]]
        if (colNameExpand && '' %in% mcRow) {
          colNameStartPos   <- which(mcRow != '')
          colNameColsToSpan <- c(colNameStartPos[-1], length(mcRow) + 1) - colNameStartPos
          if (is.null(spacerColumns) && SETable) {
            colNameColsToSpan <- colNameColsToSpan * 2            
          }
          else if (!is.null(spacerColumns) &&  SETable) {
            colNameColsToSpan <- colNameColsToSpan * 3 - 1
          }
          else if (!is.null(spacerColumns) && !SETable) {
            colNameColsToSpan <- colNameColsToSpan * 2 - 1
          }        
          colNameColsToSpan[colNameColsToSpan == 0] <- 1
          mcRow <- mcRow[colNameStartPos]
          mcRow <- paste0('        \\multicolumn{', colNameColsToSpan, '}{c}{', mcRow, '}')
        }
        else {  # if !colNameExpand
          if (SETable) {
            mcRow <- paste0('        \\multicolumn{2}{c}{', mcRow, '}')
          } else {
            mcRow <- paste0('        \\multicolumn{1}{c}{', mcRow, '}')
          }
        }
        mcRow <- str_pad(mcRow, max(nchar(mcRow)), side = 'right')
        mcRow <- paste(mcRow, '&')  # note: pastes a trailing ampersand that I'll later need to amputate
        if (colNameExpand && !is.null(spacerColumns)) {
          # If at this point, assume that there is a spacer column between 
          # each pair of adjacent \multicolumn commands.  [2014 04 27]
          mcRow[-length(mcRow)] <- paste0(mcRow[-length(mcRow)], '&')
        }
        # Adjust placement and spacing of ampersands.  This code exists 
        # partly to ensure that the correct number of ampersands appear in the
        # \multicolumn commands, and partly to ensure that the \multicolumn
        # commands are pretty-printed with correct indentation.  [2012 07 24] 
        if (is.null(rowNames) && 0 %in% spacerColumns) {
          mcRow    <- gsub('  \\m', '    \\m', mcRow,    fixed = TRUE)
          mcRow[1] <- sub ('  \\m', '& \\m',   mcRow[1], fixed = TRUE)  
        }
        else if (!is.null(rowNames) && !(0 %in% spacerColumns)) {
          mcRow    <- gsub('  \\m', '    \\m', mcRow,    fixed = TRUE)  
          mcRow[1] <- sub ('  \\m', '& \\m',   mcRow[1], fixed = TRUE)  
        }
        else if (!is.null(rowNames) && 0 %in% spacerColumns) {
          mcRow    <- gsub(' \\m',   '    \\m',  mcRow,    fixed = TRUE)  # adjust spacing for pretty-printing
          mcRow[1] <- sub ('   \\m', '&& \\m',   mcRow[1], fixed = TRUE)  # add spacer column between rowname and first column
        }
        
        # Adjust last \multicolumn command.  [2012 07 23]
        if (!colNameExpand && SETable) {
          last.mcRowPos <- ncol / 2
        } 
        else if (!colNameExpand && !SETable) {
          last.mcRowPos <- length(mcRow) 
        } 
        else if (colNameExpand && SETable) {
          last.mcRowPos <- length(mcRow)
        } 
        else if (colNameExpand && !SETable) {
          last.mcRowPos <- length(mcRow)  
        } 
        mcRow[last.mcRowPos] <- sub('\\s*&\\s*$', '', mcRow[last.mcRowPos])   # remove " & " from end of last \multicolumn command
        mcRow[last.mcRowPos] <- paste0(mcRow[last.mcRowPos], '\\tabularnewline')
        
        # Account for most spacerColumns.  The commands here don't account for 
        # a 0 value in spacerColumns, which indicates that a spacerColumn 
        # should be placed between the rownames and the first data columns.   
        # That particular kind of spacerColumn is handled above.  [2012 07 24]
        #   If colNameExpand == TRUE, spacer columns are handled above.  The 
        # column positions indicated in spacerColumns are ignored; instead, 
        # the assumption is that spacer columns are to appear between each 
        # column (or, if SETable == TRUE, between each column pair).  
        # [2014 04 27]
        if (!colNameExpand && !is.null(spacerColumns) && SETable) {
          scPos <- spacerColumns / 2  # divide by 2 to get pos. for spacerColumns amid \multicolumn commands
          mcRow[scPos] <- paste0(mcRow[scPos], '&')
        }
        else if (!colNameExpand && !is.null(spacerColumns) && !SETable) {
          scPos <- spacerColumns 
          mcRow[scPos] <- paste0(mcRow[scPos], '&')
        }        
        outputStrings <- c(outputStrings, mcRow)
        if (spaceBetweenColnameRows && i < length(colNames)) {
          outputStrings <- c(outputStrings, '        \\addlinespace[-.025in]')
        }
      }
      
      # Add \cmidrule commands.  [2012 07 23]
      #   If expandColName == TRUE, the column positions indicated in 
      # spacerColumns are ignored.  Instead, the assumption is that spacer 
      # columns are to appear between each column (or, if SETable == TRUE,   
      # between each column pair).  [2014 04 27]
      if (!colNameExpand && SETable) {
        start      <- which(colDest == 'est') + !is.null(rowNames)
        end        <- which(colDest == 'SE')  + !is.null(rowNames)
      } 
      else if (!colNameExpand && !SETable) {
        start      <- which(colDest == 'est') + !is.null(rowNames)
        end        <- which(colDest == 'est') + !is.null(rowNames)        
      }
      else if (colNameExpand) {
        colNameColsToSpanCume <- Reduce('+', colNameColsToSpan, accumulate = TRUE)  # e.g., c(1, 3, 5) becomes c(1, 4, 9)
        for (i in 1:length(colNameColsToSpanCume)) {
          colNameColsToSpanCume[i] <- colNameColsToSpanCume[i] + i - 1  # account for spacer columns 
        }
        start <- c(colNameStartPos[1], colNameStartPos[1] + colNameColsToSpanCume[-length(colNameColsToSpanCume)] + 1)
        end   <- c(
          start[-1] - 2,                                                   # all end values except the last 
          start[length(start)] + colNameColsToSpan[length(start) - 1] - 1  # last end value
        )     
        
        # Adjust start and end to account for row names and a spacer column
        # that appears immediately after the row names.  [2014 04 27]
        start <- start + !is.null(rowNames) 
        start <- start + 0 %in% spacerColumns
        end   <- end + !is.null(rowNames) 
        end   <- end + 0 %in% spacerColumns
      }
      lrText       <- if (is.null(spacerColumns) || !SETable) '(lr)' else '' 
      cmidruleLine <- paste0('\\cmidrule', lrText, '{', start, '-', end, '}', collapse = '')
      outputStrings <- c(outputStrings, paste0('        ', cmidruleLine))      
    }    
  }
  
  # PRINT TABLE ROWS
  subRegex <- paste0('\\1', columnTierSeparator)
  spacerColumnsTmp <- spacerColumns + !is.null(rowNames)  # adjust for extra ampersand if rowNames have been added
  if (! is.null(rowNames)) {
    rowNames <- str_pad(rowNames, max(nchar(rowNames)))
    mat <- cbind(rowNames, mat)
  }
  for (i in 1:nrow(mat)) {
    matLine <- paste(mat[i,], collapse = ' & ')
    
    # Add columnTierSeparator after each column tier (estimate-SE column 
    # pair).  [2012 07 23]
    numberOfAmpersands <- length(gregexpr('&', matLine)[[1]])
    if (numberOfAmpersands %% 2 == 0) { 
      rownamePrefix <- sub('(?<=& ).*', '', matLine, perl = TRUE)
      matLine       <- sub('.*?& ', '', matLine)              # remove rownamePrefix
      matLine       <- gsub('(.*?&.*?&)', subRegex, matLine)  # do substitution
      matLine       <- paste0(rownamePrefix, matLine)         # recombine
    }
    else {
      matLine <- gsub('(.*?&.*?&)', subRegex, matLine)
    }
    
    if (!is.null(spacerColumns)) {      
      for (j in rev(sort(spacerColumnsTmp))) {
        myAmpSubRegex <- paste0('^((?:.*?&){', j, '})')
        matLine <- sub(myAmpSubRegex, '\\1&', matLine)
      }
    }      
    
    # When rowNames is NULL but a spacer column appears at position 0, matLine
    # starts with an ampersand right next to a digit.  This line adds a space
    # between the ampersand and the digit.  [2012 07 25]
    matLine <- sub('^&(\\d)', '& \\1', matLine)
    matLine <- paste0('        ', matLine, '\\tabularnewline')
    
    # Add matLine to vector of rows to be printed.
    outputStrings <- c(outputStrings, matLine)
    
    # Add spacer rows.  [2015 02 14]
    if (i %in% spacerRows) {
      outputStrings <- c(outputStrings, paste0('        \\addlinespace[', spacerRowsHeight, ']'))
    }
  }
  
  # CREATE FOOTER
  if (headerFooter) {
    if (! is.null(footerRows)) {
      outputStrings <- c(outputStrings, '        \\addlinespace[.15in]')
      for (i in footerRows) {
        footerRow <- new_stack(unlist(i))
        
        # Break off the rowname.  [2012 07 25]
        if (! is.null(footerRow)) {
          footerRowName <- unshift(footerRow)
          footerRowIsSpacer <- grepl('^\\\\addlinespace', footerRowName)
          outputStrings <- c(outputStrings, paste0('        ', footerRowName))
          if (0 %in% spacerColumns & !footerRowIsSpacer) {
            outputStrings <- c(outputStrings, ' && ')      
          }
          else if (!footerRowIsSpacer) {
            outputStrings <- c(outputStrings, ' & ')            
          }
        }
        
        # Eliminate leading zeroes for R^2.  [2013 03 14]
        if (footerRowName %in% c('$R^2$', 'R$^2$')) {
          footerRow$.Data <- gsub('^0(\\.\\d+)$', '\\1', footerRow$.Data)
        }
        
        # Add trailing zeroes for R^2 and SER, e.g., change "1.9" to "1.90" so 
        # that it matches up with all of the other SERs, which will have more 
        # digits after the decimal place.  This code should always give the  
        # SER exactly two decimal places.
        if (footerRowName %in% c('$R^2$', 'SER', 'Standard error of regression')) {
          for (i in 1: length(footerRow$.Data)) {
            footerRow$.Data[i] <- sub('^(\\d*\\.?\\d)$', '\\10', footerRow$.Data[i])
          }
        }
        
        if (!footerRowIsSpacer) { 
          # Construct the \multicolumn statements for footerRow.  [2012 07 25]
          footerRow           <- paste0('          \\multicolumn{2}{c}{', footerRow$.Data, '} &')
          footerRow[ncol / 2] <- sub('\\s*&\\s*', '', footerRow[ncol / 2])
          footerRow[ncol / 2] <- paste0(footerRow[ncol / 2], '\\tabularnewline')
          
          # Account for most spacerColumns.  The commands here don't account for 
          # a 0 value in spacerColumns, which indicates that a spacerColumn should
          # be placed between the rownames and the first data columns.  That 
          # particular kind of spacerColumn is handled above.  [2012 07 24]  
          if (!is.null(spacerColumns)) {
            scPos <- spacerColumns / 2  # divide by 2 to get pos. for spacerColumns amid \multicolumn commands
            footerRow[scPos] <- paste0(footerRow[scPos], '&')
          }
          outputStrings <- c(outputStrings, footerRow)
        }
      }
    }
    
    outputStrings <- c(outputStrings, '        \\bottomrule')
    outputStrings <- c(outputStrings, '      \\end{tabular}')
    if (printCaption) {
      if (is.null(captionMargins)) {
        outputStrings <- c(outputStrings, '      %\\captionsetup{margin={.75in, .75in}}')
      } else {
        outputStrings <- c(outputStrings, paste0('      \\captionsetup{margin={', captionMargins[1], ', ', captionMargins[2], '}}'))
      }
      outputStrings <- c(outputStrings, '      \\caption{%')
      outputStrings <- c(outputStrings, paste0('        \\label{', label, '}%'))
      outputStrings <- c(outputStrings, paste0('        ', caption))
      outputStrings <- c(outputStrings, '      }')
    }
    outputStrings <- c(outputStrings, '    \\end{center}')
    if (starredFloat) {
      outputStrings <- c(outputStrings, '  \\end{table*}')
    } else {
      outputStrings <- c(outputStrings, '  \\end{table}')
    }
    outputStrings <- c(outputStrings, '}%')
    if (callCommand) {
      if (landscape) {
        outputStrings <- c(outputStrings, '\\afterpage{')
        outputStrings <- c(outputStrings, '  \\begin{landscape}')
        outputStrings <- c(outputStrings, '    \\thispagestyle{empty}')
        outputStrings <- c(outputStrings, paste0('    \\', commandName, '{t}'))
        outputStrings <- c(outputStrings, '  \\end{landscape}')
        outputStrings <- c(outputStrings, '  \\clearpage')
        outputStrings <- c(outputStrings, '}')
      }
      else {
        outputStrings <- c(outputStrings, paste0('\\', commandName, '{p}'))
      }
    }
  }
  
  class(outputStrings) <- c('latexTable', class(outputStrings)) 
  if (writeToClipboard && Sys.info()['sysname'] == 'Windows') {
    writeClipboard(paste0(outputStrings, collapse = "\n"))
  }
  outputStrings
}  

print.latexTable <- function (tab) { 
  for (i in 1:length(tab)) {
    writeLines(tab[i])
  }
}